﻿using SharpDepend.Datatypes;
using SoundEngine3;
using SoundEngine3.AudioFile;
using SoundEngine3.AudioProcessing;
using SoundEngine3.AudioProcessing.Algorithms.SampleInterpolations;
using SoundEngine3.DspPlugins;
using SoundEngine3.DspPlugins.Meta;
using SoundEngine3.MIDI.Meta;
using System;
using System.Collections.Generic;
using System.IO;
using ZquenceStudio3.Core;
using ZquenceStudio3.Core.DspPlugins;
using ZquenceStudio3.Core.DspPlugins.Attributes;
using ZquenceStudio3.DspPlugins;
using ZquenceStudio3.Core.Album;
using ZquenceStudio3.Core.Storage;
using ZquenceStudio3.Core.Utils;
using SharpDepend.Storage;

namespace ZquenceStudio3Host.DspPlugins.Synthesizers
{
    public class SamplerSynthDescription : IDspPlugin
    {
        public DspPluginInfo GetDspPluginInfo()
        {
            return new DspPluginInfo("SamplerSynth", DspType.Synthesizer);
        }

        public BaseDspPluginInstance CreateDsp(ZquenceStudio3Context context, DspPluginInfo info)
        {
            return new SamplerSynth(context, info);
        }
    }

    public interface ISamplerSynthUI
    {
        bool LoopIsActivated { get; set; }

        void RemoveLabelInNote(int note);
        void InsertLabelInNote(int note);
    }

    public class SamplerSynth : CoreDspSynthesizerPlugin
    {
        struct OngoingAudioSample : IOngoingSample
        {
            public static readonly OngoingAudioSample Default = new OngoingAudioSample();
            
            public double StartSample { get; set; }
            public double Pitch { get; set; }
            public bool Running { get; set; }
            public double ProcessedSamples { get; set; }
            public AudioBase Audio { get; set; }
            public double StopSample { get; set; }
            public byte ID { get; internal set; }
            public int UniqueId { get; internal set; }
            public bool Loop { get; set; }
            public bool Disposed { get; set; }

            public double Total
            {
                get
                {
                    return StopSample - StartSample;
                }
            }

            public double Volyme { get; set; }
            public double Pan { get; set; }
            public double LastPitch { get; set; }
            public double LastPan { get; set; }
            public double LastVolyme { get; internal set; }

            public void SetMidi(ref MidiMessage message)
            {
                Volyme = LastVolyme = message.MidiData.Parameter2 / 127.0;
                Pan = LastPan = message.Pan / 128.0;
            }
        }

        internal class AudioBaseWithPath
        {
            public AudioBase Audio { get; set; }
            public ContentFile File { get; set; }
        }
        
        private const int ITEM_HEIGHT = 30;
        private List<MidiMessage> mMidiMessages;
        private List<OngoingAudioSample> mOngoingSamples;
        private Dictionary<int, AudioBaseWithPath> m_audios = new Dictionary<int, AudioBaseWithPath>();

        internal ParameterPropertiesDirect volyme;
        internal ParameterPropertiesDirect pitch;
        internal ParameterPropertiesDirect note;
        internal ParameterPropertiesDirect looping;
        
        private List<int> mReadyLabels = new List<int>();

        private static Color s_itemColor = new Color(64, 66, 68, 80);
        private static int UniqueIdNext;

        private ISamplerSynthUI mSamplerSynthUI;

        internal void SetUI(ISamplerSynthUI ui)
        {
            mSamplerSynthUI = ui;
        }

        /// <summary>
        /// Get or set the main volyme.
        /// </summary>
        public double Volyme { get; set; }

        /// <summary>
        /// Get or set pitch property for sample.
        /// </summary>
        public double Pitch { get; private set; }

        /// <summary>
        /// Get or set tune property for sample.
        /// </summary>
        public double Tune { get; private set; }

        /// <summary>
        /// Get or set loop property for sample.
        /// </summary>
        public bool Loop { get; private set; }

        public SamplerSynth(ZquenceStudio3Context application, DspPluginInfo dspInfo)
            : base(application, dspInfo)
        {
            mMidiMessages = new List<MidiMessage>();
            mOngoingSamples = new List<OngoingAudioSample>();

            Volyme = 1;

            SupportAudioProcessing = true;

            AddAttribute(new EmbeddedContentAttribute());
            
            // Volyme.
            volyme = new ParameterPropertiesDirect(this);
            volyme.Label = "Volyme";
            volyme.DirectGet += delegate () { return Volyme; };
            volyme.DirectSet += delegate (double value) { Volyme = value; };
            volyme.MinValue = 0;
            volyme.MaxValue = 2.0;
            AddParameter(volyme);

            // Pitch.
            pitch = new ParameterPropertiesDirect(this);
            pitch.Label = "Pitch";
            pitch.DirectGet += delegate () { return Pitch; };
            pitch.DirectSet += delegate (double value) { Pitch = value; };
            pitch.MinValue = -1.0;
            pitch.MaxValue = +1.0;
            AddParameter(pitch);

            // Note.
            note = new ParameterPropertiesDirect(this);
            note.Label = "Tune";
            note.DirectGet += delegate () { return Tune; };
            note.DirectSet += delegate (double value) { Tune = value; };
            note.MinValue = -12.0;
            note.MaxValue = +12.0;
            AddParameter(note);

            // Loop.
            looping = new ParameterPropertiesDirect(this);
            looping.Label = "Loop";
            looping.DirectGet += delegate () { return Loop ? 1 : 0; };
            looping.DirectSet += delegate (double value)
            {
                Loop = value >= 0.5 ? true : false;
            };
            looping.MinValue = 0.0;
            looping.MaxValue = 1.0;
            AddParameter(looping);
        }

        internal AudioBaseWithPath GetAudioBase(string filenameWithExtension)
        {
            foreach (var audio in m_audios)
            {
                if (audio.Value.File.FilenameWithExtension.Equals(filenameWithExtension))
                {
                    return audio.Value;
                }
            }

            return null;
        }

        internal ContentFile GetContentFileByFilename(string path)
        {
            string filenameWithExtension = StringUtils.GetFilenameOrFolderNameFromFullPath(path);

            foreach (var contentFile in ContentFiles)
            {
                if (contentFile.FilenameWithExtension.Equals(filenameWithExtension))
                {
                    return contentFile;
                }
            }

            return null;
        }

        protected override void PackData(CustomNode node)
        {
            CustomNode notes = new CustomNode(node);
            notes.Name = "Notes";

            if (GetAttribute<EmbeddedContentAttribute>().Value)
            {
                // Save content as embedded.
                foreach (var item in m_audios)
                {
                    CustomNode note = new CustomNode(notes);

                    // Save audio to the album path.
                    string rpath = item.Value.File.RelativeFilePath;

                    string path = item.Value.File.FromAlbumFilePath;

                    // TODO: Spara alla audio-filer inbackat i CustomNode...
                    using (var stream = SharpDepend.Manager.FileManager.OpenFile(path))
                    {
                        note.AddProperty("IsEmbedded", true);
                        note.AddProperty("K", item.Key);
                        note.AddProperty("P", rpath);
                        note.AddProperty("S", HexConverter.ToHex(stream));
                    }
                }
            }
            else
            {
                // Save content referenced from album.
                foreach (var item in m_audios)
                {
                    CustomNode note = new CustomNode(notes);

                    // Save audio to the album path.
                    string path = item.Value.File.RelativeFilePath;
                    ContentFile content = CoreContext.GetContentFileByFileItem(path);
                    
                    note.AddProperty("K", item.Key);
                    note.AddProperty("F", content.FilenameWithExtension);
                }
            }
        }

        protected override void UnpackData(CustomNode node)
        {
            CustomNode notes = node.Get("Notes");

            foreach (var note in notes.Nodes)
            {
                bool isEmbedded = note.GetPropertyValue<bool>("IsEmbedded", false);

                if (isEmbedded)
                {
                    int key = note.GetPropertyValue<int>("K");
                    string path = note.GetPropertyValue("P");

                    ContentFile file = null;

                    // Try to get existing Content file, otherwise null will be returned.
                    file = CoreContext.GetContentFileByFileItem(path);

                    // Otherwise check if file is excluded from the album, then add it if available.
                    if (file == null)
                    {
                        // Try to find excluded file to include if available.
                        string relativePath = CoreContext.CurrentAlbum.Filename + "/" + path;
                        string excludedFilePath = CoreContext.CurrentAlbum.SolutionFolder + "/" + relativePath;
                        if (SharpDepend.Manager.FileManager.PathExist(excludedFilePath, false))
                        {
                            // Include it to the album.
                            file = CoreContext.IncludeRelativeFileAsContentFile(relativePath);
                        }
                    }

                    Stream streamContent = null;
                    
                    // If no file found. Then create a new file and include it with the embedded audio data.
                    if (file == null)
                    {
                        string contentFilePath = path;

                        // Create a new one if content do not exist.
                        file = CoreContext.CreateUniqueContentFile(contentFilePath, out streamContent);

                        string hex = note.GetPropertyValue<string>("S", null);
                        if (hex != null)
                        {
                            using (MemoryStream stream = HexConverter.FromHex<MemoryStream>(hex))
                            {
                                using (streamContent)
                                {
                                    stream.CopyTo(streamContent);
                                }
                                
                                LoadAudioFromFile(file.FromAlbumFilePath, key, stream);
                            }
                        }
                    }
                    else
                    {
                        // Load the already existed file.
                        using (streamContent = file.GetStream())
                        {
                            LoadAudioFromFile(file.FromAlbumFilePath, key, streamContent);
                        }
                    }
                }
                else
                { 
                    int key = note.GetPropertyValue<int>("K");
                    string filename = note.GetPropertyValue("F");

                    ContentFile file = CoreContext.GetContentFileByFileItem(filename);

                    if (file != null)
                    {
                        _LoadAudioFromContent(key, null, file);
                    }
                    else
                    {
                        // Display what file that was missing when synthesizer has been loaded.
                        AddError("'" + filename + "' was not found included in album.");
                    }
                }
            }
        }                     
        
        public override Version Version
        {
            get { return DspPluginInfo.DefaultVersion; }
        }

        public override string PluginInfo
        {
            get { return DspPluginInfo.DefaultPluginInfo; }
        }
        
        protected override void OnProcessEvents(List<MidiMessage> midiMessages)
        {
            foreach (var data in midiMessages)
            {
                // Only process note on status.
                if (data.MidiData.Status == MidiStatus.NOTE_ON || data.MidiData.Status == MidiStatus.NOTE_OFF)
                {
                    mMidiMessages.Add(data);
                }
            }
        }

        public override void OnProcess(BaseAudioBuffer output)
        {
            if (m_audios.Count > 0)
            {
                // Clear buffer with zero.
                output.Clear();

                if (mMidiMessages.Count > 0)
                {
                    // Loop incomming midi notes.
                    for (int i = 0; i < mMidiMessages.Count; i++)
                    {
                        MidiMessage message = mMidiMessages[i];
                        
                        if (message.MidiData.Status == MidiStatus.NOTE_ON)
                        {
                            OngoingAudioSample ongoingSample = OngoingAudioSample.Default;
                            ongoingSample.UniqueId = ++UniqueIdNext;

                            bool loop = Loop;

                            ongoingSample.SetMidi(ref message);
                            ongoingSample.Volyme *= Volyme;
                            ongoingSample.Loop = loop;
                            ongoingSample.ID = message.MidiData.Parameter1;
                            ongoingSample.StartSample = message.DeltaFrame;
                            ongoingSample.Running = true;
                            ongoingSample.Pitch = message.MidiData.Parameter1;

                            int audioNote = 0;
                            AudioBase currentAudio = GetNearestAudio(ref ongoingSample, ref audioNote);
                            
                            int n = message.MidiData.Parameter1 - audioNote;

                            if (n == 0)
                            {
                                ongoingSample.LastPitch = ongoingSample.Pitch = 1.0;
                            }
                            else
                            {
                                ongoingSample.LastPitch = ongoingSample.Pitch = 1.0 * Math.Exp(0.69314718 * (n / 12.0));
                            }

                            ongoingSample.Audio = currentAudio;
                            ongoingSample.StopSample = loop ? double.MaxValue : currentAudio.Length;

                            mOngoingSamples.Add(ongoingSample);

                            // Remove processed incomming note.
                            //mMidiMessages.RemoveAt(i);
                            //--i;
                        }
                        else if (message.MidiData.Status == MidiStatus.NOTE_OFF)
                        {
                            for (int j = 0; j < mOngoingSamples.Count; j++)
                            {
                                var ongoingSample = mOngoingSamples[j];
                                if (ongoingSample.ID == message.MidiData.Parameter1 && !ongoingSample.Disposed)
                                {
                                    if (ongoingSample.Loop)
                                    {
                                        ongoingSample.StopSample = message.DeltaFrame;
                                    }
                                    ongoingSample.Disposed = true;
                                    mOngoingSamples[j] = ongoingSample;

                                    // Remove processed incomming note.
                                    // TODO: Varför behöver vi göra såhär?
                                    //if (mMidiMessages.Count > 0)
                                    //{
                                    //    mMidiMessages.RemoveAt(i);
                                    //    --i;
                                    //}
                                }
                            }
                        }
                    }

                    mMidiMessages.Clear();
                }

                if (mOngoingSamples.Count > 0)
                {
                    //double pAdd = 1.0 * SoundMath.Pow_Note(Pitch + Tune);
                    double pAdd = Math.Exp(0.69314718 * ((Pitch + Tune) / 12.0));
                    for (int i = mOngoingSamples.Count - 1; i >= 0; i--)
                    {
                        OngoingAudioSample sample = mOngoingSamples[i];

                        double samplePitch = sample.Pitch * pAdd * SoundEngineContext.SampleRatePitch(sample.Audio.SourceSampleRate);
                        InterpolationState state;

                        double size = 0;

                        if (sample.ProcessedSamples == 0.0)
                        {
                            state = InterpolationState.Begin;
                            sample.LastVolyme = sample.Volyme;
                            sample.LastPan = sample.Pan;
                            sample.LastPitch = samplePitch;
                        }
                        else
                        {
                            state = InterpolationState.Continue;
                        }

                        int interpolationId = sample.UniqueId;
                        bool isAtEnd = sample.StopSample <= SoundEngineContext.BufferSize;
                        if (isAtEnd)
                        {
                            state = InterpolationState.End;
                        }
                        
                        // If Mono.
                        if (output.Channels.Length == 1)
                        {
                            Channel mono = output.Channels[0];

                            int length = mono.BufferSize;
                            if (sample.Loop)
                            {
                                length = (int)System.Math.Min(length, sample.StopSample);
                            }

                            size = sample.Audio.Process(mono, null, state, interpolationId, sample.ProcessedSamples, System.Math.Max(0, sample.StartSample), length, sample.Loop
                                , sample.LastPitch, samplePitch
                                , sample.LastVolyme, sample.Volyme
                                , sample.LastPan, sample.Pan);
                        }
                        // If Stereo.
                        else if (output.Channels.Length > 1)
                        {
                            Channel left = output.Channels[0];
                            Channel right = output.Channels[1];

                            int length = left.BufferSize;
                            if (sample.Loop)
                            {
                                length = (int)System.Math.Min(length, sample.StopSample);
                            }

                            size = sample.Audio.Process(left, right, state, interpolationId, sample.ProcessedSamples, System.Math.Max(0, sample.StartSample), length, sample.Loop
                                , sample.LastPitch, samplePitch
                                , sample.LastVolyme, sample.Volyme
                                , sample.LastPan, sample.Pan);
                        }

                        sample.StartSample -= SoundEngineContext.BufferSize;
                        sample.ProcessedSamples += size;

                        // Check if we have reach the end.
                        if (isAtEnd || size == 0)
                        {
                            Channel left = output.Channels[0];
                            Channel right = output.Channels[1];
                            sample.Audio.Process(left, right, InterpolationState.End, interpolationId, sample.ProcessedSamples, System.Math.Max(0, sample.StartSample), 0, sample.Loop
                                   , sample.LastPitch, samplePitch
                                   , sample.LastVolyme, sample.Volyme
                                   , sample.LastPan, sample.Pan);
                            mOngoingSamples.RemoveAt(i);
                            continue;
                        }

                        sample.LastPitch = samplePitch;
                        sample.LastPan = sample.Pan;
                        sample.LastVolyme = sample.Volyme;
                        mOngoingSamples[i] = sample;
                    }
                }
            }
        }
        
        private AudioBase GetNearestAudio(ref OngoingAudioSample actionNote, ref int audioNote)
        {
            // TODO: Går att optimera. Gör så att den minsta noten i mappen sparas i en separat medlems-variabel.
            //       Och sen ifall ingen not kan mappas (ifall noten är under första noten) så tar han den första noten med hjälp av medlemsvariabeln.

            // Check.
            if (m_audios.Count == 0)
            {
                throw new Exception("No audio is not mappen. Can not get nearest audio.");
            }

            // Find nearest mapped note.
            audioNote = -1;
            foreach (var audio in m_audios)
            {
                if (actionNote.Pitch >= audio.Key && audio.Key > audioNote)
                {
                    // Got nearest note from down.
                    audioNote = audio.Key;
                }
            }

            // If need to get the first note.
            if (audioNote == -1)
            {
                // Problably under the first note. Just take the first note.
                int minimumNote = 999;
                foreach (var audio in m_audios)
                {
                    if (audio.Key < minimumNote)
                    {
                        minimumNote = audio.Key;
                    }
                }
                audioNote = minimumNote;
            }

            AudioBaseWithPath currentAudio = m_audios[audioNote];
            return currentAudio.Audio;
        }

        private AudioBase GetExistedAudio(int note)
        {
            // If note already has been mappend.
            if (m_audios.ContainsKey(note))
            {
                return m_audios[note].Audio;
            }
            else
            {
                return null; // Audio is not mapped.
            }
        }

        internal bool AudioExist(int note)
        {
            return m_audios.ContainsKey(note);
        }

        internal void LoadAudio(AudioBaseWithPath audio, int note)
        {
            // If note already has been mappend.
            if (m_audios.ContainsKey(note))
            {
                // Remove old content file from album if no other is using it.
                var oldFileItem = m_audios[note].File;
                
                if (audio == null)
                {
                    m_audios.Remove(note);
                }
                else
                {
                    m_audios[note] = audio;
                }

                // If audio do not exist here anymore. Remove it.
                if (GetAudioBase(oldFileItem.FilenameWithExtension) == null)
                {
                    if (ContentFiles.Contains(oldFileItem))
                    {
                        RemoveContentFile(oldFileItem);
                    }
                }
            }
            else
            {
                if (audio != null)
                {
                    m_audios.Add(note, audio);
                }
            }
        }

        internal void ClearAudioSamples()
        {
            List<int> audios = new List<int>(m_audios.Keys);
            foreach (var audio in audios)
            {
                UnloadAudio(audio);
            }
        }

        internal void UnloadAudio(int note)
        {
            mSamplerSynthUI.RemoveLabelInNote(note);
            LoadAudio(null, note);
        }
        
        /// <summary>
        /// Calls from internal.
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="note"></param>
        /// <param name="customStream"></param>
        private void LoadAudioFromFile(string filePath, int note, Stream customStream)
        {
            // Add file to album and attach to this dsp.
            ContentFile content = GetContentFileByFilename(filePath);
            if (content == null)
            {
                string relativePath = StringUtils.GetFilenameOrFolderNameFromFullPath(filePath);
                ContentFile contentFromAlbum = this.CoreContext.GetContentFileByFileItem(relativePath);

                if (contentFromAlbum == null)
                {
                    // Add to album.
                    content = CoreContext.CopyFileItemToAlbum(filePath);

                    // Make this content file removable when not used.
                    content.AllowAutomaticRemoveWhenNotUsed = true;
                }
                else
                {
                    content = contentFromAlbum;
                }

                // Add content file to this SamplerSynth.
                AddContentFile(content);
            }

            _LoadAudioFromContent(note, customStream, content);
        }

        private void _LoadAudioFromContent(int note, Stream customStream, ContentFile content)
        {
            // Create audio from file.
            AudioBase audioFile = null;

            if (customStream == null)
            {
                // Check if file exist. Otherwise audioFile will be null and no audio will be loaded.
                if(CoreContext.ContentFileExistOnDisk(content))
                {
                    using (var stream = CoreContext.OpenContentFile(content))
                    {
                        audioFile = SoundEngineContext.CreateAudioSampleByStream(stream);
                    }
                }
                else
                {
                    // Display what file that was missing when synthesizer has been loaded.
                    AddError("'" + content.FromAlbumFilePath + "' was not found on disk."); // TODO: När vi jämnför c2.wav på original och i album så ska vi jämnföra innehållet!
                }
            }
            else
            {
                audioFile = SoundEngineContext.CreateAudioSampleByStream(customStream);
            }

            // Load audio only if file exist or available.
            if (audioFile != null)
            {
                // Add a char on note if an audio is mapped.
                if (!AudioExist(note))
                {
                    mSamplerSynthUI.InsertLabelInNote(note);
                }

                // Try to get cached audio.
                AudioBaseWithPath audio = GetAudioBase(content.FilenameWithExtension);

                // If not yet cached, create one.
                if (audio == null)
                {
                    audio = new AudioBaseWithPath { Audio = audioFile, File = content };
                }

                // Insert audio to the audio sampler synth.
                LoadAudio(audio, note);
            }
        }
        
        public override void Dispose()
        {
            base.Dispose();
            
            // Loop all mapped audio files.
            foreach (var audio in m_audios)
            {
                // Several notes can be mapped to the same audio file. Check so the content
                // file exist, otherwise it may have been removed erlier.
                if (ContentFiles.Contains(audio.Value.File))
                {
                    // Remove content files.
                    RemoveContentFile(audio.Value.File);
                }
            }
            m_audios.Clear();
        }

        /// <summary>
        /// Return name of audio matched to the note key, otherwise empty string will be returned.
        /// </summary>
        public override string GetNoteKeyLabel(int noteKey)
        {
            if (m_audios.ContainsKey(noteKey))
            {
                return StringUtils.GetFilenameFromFullPath(m_audios[noteKey].File.FromAlbumFilePath);
            }
            else
            {
                return string.Empty;
            }
        }
    }
}
